<?php
/**
 * Converts to and from JSON format.
 *
 * JSON (JavaScript Object Notation) is a lightweight data-interchange
 * format. It is easy for humans to read and write. It is easy for machines
 * to parse and generate. It is based on a subset of the JavaScript
 * Programming Language, Standard ECMA-262 3rd Edition - December 1999.
 * This feature can also be found in  Python. JSON is a text format that is
 * completely language independent but uses conventions that are familiar
 * to programmers of the C-family of languages, including C, C++, C#, Java,
 * JavaScript, Perl, TCL, and many others. These properties make JSON an
 * ideal data-interchange language.
 *
 * This package provides a simple encoder and decoder for JSON notation. It
 * is intended for use with client-side Javascript applications that make
 * use of HTTPRequest to perform server communication functions - data can
 * be encoded into JSON notation for use in a client-side javascript, or
 * decoded from incoming Javascript requests. JSON format is native to
 * Javascript, and can be directly eval()'ed with no further parsing
 * overhead
 *
 * All strings should be in ASCII or UTF-8 format!
 *
 * PHP versions 4 and 5
 *
 * LICENSE: Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met: Redistributions of source code must retain the
 * above copyright notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 *
 * @category
 * @package
 * @author		Michal Migurski <mike-json@teczno.com>
 * @author		Matt Knapp <mdknapp[at]gmail[dot]com>
 * @author		Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
 * @copyright	2005 Michal Migurski
 * @reference
 * @license 	http://www.freebsd.org/copyright/freebsd-license.html
 * @link		http://pear.php.net/pepr/pepr-proposal-show.php?id=198
 */

/**
 * Marker constant for JSON::decode(), used to flag stack state
 */
define('JSON_SLICE',   1);

/**
 * Marker constant for JSON::decode(), used to flag stack state
 */
define('JSON_IN_STR',  2);

/**
 * Marker constant for JSON::decode(), used to flag stack state
 */
define('JSON_IN_ARR',  4);

/**
 * Marker constant for JSON::decode(), used to flag stack state
 */
define('JSON_IN_OBJ',  8);

/**
 * Marker constant for JSON::decode(), used to flag stack state
 */
define('JSON_IN_CMT', 16);

/**
 * Behavior switch for JSON::decode()
 */
define('JSON_LOOSE_TYPE', 10);

/**
 * Behavior switch for JSON::decode()
 */
define('JSON_STRICT_TYPE', 11);

/**
 * encodes an arbitrary variable into JSON format
 *
 * @param	mixed	$var	any number, boolean, string, array, or object to be encoded.
 *				see argument 1 to JSON() above for array-parsing behavior.
 *				if var is a strng, note that encode() always expects it
 *				to be in ASCII or UTF-8 format!
 *
 * @return	 string  JSON string representation of input var
 * @access	 public
 */
function json_encode2($var) {
	switch (gettype($var)) {
		case 'boolean':
			return $var ? 'true' : 'false';
		case 'NULL':
			return 'null';
		case 'integer':
			return sprintf('%d', $var);
		case 'double':
		case 'float':
			return sprintf('%f', $var);
		case 'string':
			$var = str_replace("\n", "\\n", $var);
			$var = str_replace("\r", "\\r", $var);
			$var = str_replace("\"", "\\\"", $var);
			return ("\"$var\"");
		case 'array':
			/*
			 * As per JSON spec if any array key is not an integer
			 * we must treat the the whole array as an object. We
			 * also try to catch a sparsely populated associative
			 * array with numeric keys here because some JS engines
			 * will create an array with empty indexes up to
			 * max_index which can cause memory issues and because
			 * the keys, which may be relevant, will be remapped
			 * otherwise.
			 *
			 * As per the ECMA and JSON specification an object may
			 * have any string as a property. Unfortunately due to
			 * a hole in the ECMA specification if the key is a
			 * ECMA reserved word or starts with a digit the
			 * parameter is only accessible using ECMAScript's
			 * bracket notation.
			*/
			// treat as a JSON object
			if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
				return (sprintf('{%s}', join(',', array_map('_name_value', array_keys($var), array_values($var)))));
			}
			// treat it like a regular array
			return (sprintf('[%s]', join(',', array_map('json_encode2', $var))));
		case 'object':
			$vars = get_object_vars($var);
			return (sprintf('{%s}', join(',', array_map('_name_value', array_keys($vars), array_values($vars)))));
		default:
			return ("");
	}
}

/** function _name_value
 * array-walking function for use in generating JSON-formatted name-value pairs
 *
 * @param	string	$name	name of key to use
 * @param	mixed	$value	reference to an array element to be encoded
 *
 * @return	 string  JSON-formatted name-value pair, like '"name":value'
 * @access	 private
*/
function _name_value($name, $value) {
	return (sprintf("%s:%s", json_encode2(strval($name)), json_encode2($value)));
}

/**
 * reduce a string by removing leading and trailing comments and whitespace
 *
 * @param	$str	string	  string value to strip of comments and whitespace
 *
 * @return	 string  string value stripped of comments and whitespace
 * @access	 private
*/
function _reduce_string($str) {
	$str = preg_replace(array(
		// eliminate single line comments in '// ...' form
		'#^\s*//(.+)$#m',
		// eliminate multi-line comments in '/* ... */' form, at start of string
		'#^\s*/\*(.+)\*/#Us',
		// eliminate multi-line comments in '/* ... */' form, at end of string
		'#/\*(.+)\*/\s*$#Us'
		), '', $str);

	// eliminate extraneous space
	return (trim($str));
}

/**
 * decodes a JSON string into appropriate variable
 *
 * @param	string	$str	JSON-formatted string
 *
 * @return	 mixed	 number, boolean, string, array, or object
 *				   corresponding to given JSON input string.
 *				   See argument 1 to JSON() above for object-output behavior.
 *				   Note that decode() always returns strings
 *				   in ASCII or UTF-8 format!
 * @access	 public
*/
function json_decode2($str, $always_true=TRUE) {
	$str = _reduce_string($str);
	switch (strtolower($str)) {
		case 'true':
			return true;
		case 'false':
			return false;
		case 'null':
			return null;
		default:
			if (is_numeric($str)) {
				// Lookie-loo, it's a number
				// This would work on its own, but I'm trying to be
				// good about returning integers where appropriate:
				// return (float)$str;

				// Return float or int, as appropriate
				return ((float)$str == (integer)$str)
					? (integer)$str
					: (float)$str;
			} elseif (preg_match('/^("|\').+("|\')$/s', $str, $m) && $m[1] == $m[2]) {
				// STRINGS RETURNED IN UTF-8 FORMAT
				$delim = substr($str, 0, 1);
				$chrs = substr($str, 1, -1);
				$utf8 = '';
				$strlen_chrs = strlen($chrs);

				for ($c = 0; $c < $strlen_chrs; ++$c) {
					$substr_chrs_c_2 = substr($chrs, $c, 2);
					$ord_chrs_c = ord($chrs{$c});
					switch ($substr_chrs_c_2) {
						case '\b':	$utf8 .= chr(0x08);  $c+=1;  break;
						case '\t':	$utf8 .= chr(0x09);  $c+=1;  break;
						case '\n':	$utf8 .= chr(0x0A);  $c+=1;  break;
						case '\f':	$utf8 .= chr(0x0C);  $c+=1;  break;
						case '\r':	$utf8 .= chr(0x0D);  $c+=1;  break;
						case '\\"':
						case '\\\'':
						case '\\\\':
						case '\\/':
							if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
							   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
								$utf8 .= $chrs{++$c};
							}
							break;
						default:
							if (preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6))) {
								// single, escaped unicode character
								$utf16 = chr(hexdec(substr($chrs, ($c+2), 2)))
									   . chr(hexdec(substr($chrs, ($c+4), 2)));
								$utf8 .= mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
								$c+=5;
							} elseif(($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F)) {
								$utf8 .= $chrs{$c};
							} elseif(($ord_chrs_c & 0xE0) == 0xC0) {
								// characters U-00000080 - U-000007FF, mask 110XXXXX
								//see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= substr($chrs, $c, 2); $c += 1;
							} elseif(($ord_chrs_c & 0xF0) == 0xE0) {
								// characters U-00000800 - U-0000FFFF, mask 1110XXXX
								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= substr($chrs, $c, 3); $c += 2;
							} elseif(($ord_chrs_c & 0xF8) == 0xF0) {
								// characters U-00010000 - U-001FFFFF, mask 11110XXX
								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= substr($chrs, $c, 4); $c += 3;
							} elseif(($ord_chrs_c & 0xFC) == 0xF8) {
								// characters U-00200000 - U-03FFFFFF, mask 111110XX
								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= substr($chrs, $c, 5); $c += 4;
							} elseif(($ord_chrs_c & 0xFE) == 0xFC) {
								// characters U-04000000 - U-7FFFFFFF, mask 1111110X
								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
								$utf8 .= substr($chrs, $c, 6); $c += 5;
							}
							break;
					}
				}
				return ($utf8);
			} elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
				// array, or object notation
				if ($str{0} == '[') {
					$stk = array(JSON_IN_ARR);
					$arr = array();
				} else {
					$stk = array(JSON_IN_OBJ);
					$obj = array();
				}
				array_push($stk, array('what' => JSON_SLICE, 'where' => 0, 'delim' => false));
				$chrs = substr($str, 1, -1);
				$chrs = _reduce_string($chrs);
				if ($chrs == '') {
					if (reset($stk) == JSON_IN_ARR)
						return ($arr);
					else
						return ($obj);
				}
				$strlen_chrs = strlen($chrs);
				for ($c = 0; $c <= $strlen_chrs; ++$c) {
					$top = end($stk);
					$substr_chrs_c_2 = substr($chrs, $c, 2);
					if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == JSON_SLICE))) {
						// found a comma that is not inside a string, array, etc.,
						// OR we've reached the end of the character list
						$slice = substr($chrs, $top['where'], ($c - $top['where']));
						array_push($stk, array('what' => JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
						//print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
						if (reset($stk) == JSON_IN_ARR) {
							// we are in an array, so just push an element onto the stack
							array_push($arr, json_decode2($slice));
						} elseif (reset($stk) == JSON_IN_OBJ) {
							// we are in an object, so figure
							// out the property name and set an
							// element in an associative array,
							// for now
							if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
								// "name":value pair
								$key = json_decode2($parts[1]);
								$val = json_decode2($parts[2]);
								$obj[$key] = $val;
							} elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
								// name:value pair, where name is unquoted
								$key = $parts[1];
								$val = json_decode2($parts[2]);
								$obj[$key] = $val;
							}
						}
					} elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) &&
							 in_array($top['what'], array(JSON_SLICE, JSON_IN_ARR, JSON_IN_OBJ))) {
						// found a quote, and we are not inside a string
						array_push($stk, array('what' => JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c}));
						//print("Found start of string at {$c}\n");
					} elseif (($chrs{$c} == $top['delim']) &&
							 ($top['what'] == JSON_IN_STR) &&
							 (($chrs{$c - 1} != "\\") ||
							 ($chrs{$c - 1} == "\\" && $chrs{$c - 2} == "\\"))) {
						// found a quote, we're in a string, and it's not escaped
						array_pop($stk);
					} elseif (($chrs{$c} == '[') &&
							 in_array($top['what'], array(JSON_SLICE, JSON_IN_ARR, JSON_IN_OBJ))) {
						// found a left-bracket, and we are in an array, object, or slice
						array_push($stk, array('what' => JSON_IN_ARR, 'where' => $c, 'delim' => false));
					} elseif (($chrs{$c} == ']') && ($top['what'] == JSON_IN_ARR)) {
						// found a right-bracket, and we're in an array
						array_pop($stk);
					} elseif (($chrs{$c} == '{') &&
							 in_array($top['what'], array(JSON_SLICE, JSON_IN_ARR, JSON_IN_OBJ))) {
						// found a left-brace, and we are in an array, object, or slice
						array_push($stk, array('what' => JSON_IN_OBJ, 'where' => $c, 'delim' => false));
					} elseif (($chrs{$c} == '}') && ($top['what'] == JSON_IN_OBJ)) {
						// found a right-brace, and we're in an object
						array_pop($stk);
					} elseif (($substr_chrs_c_2 == '/*') &&
							 in_array($top['what'], array(JSON_SLICE, JSON_IN_ARR, JSON_IN_OBJ))) {
						// found a comment start, and we are in an array, object, or slice
						array_push($stk, array('what' => JSON_IN_CMT, 'where' => $c, 'delim' => false));
						$c++;
					} elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == JSON_IN_CMT)) {
						// found a comment end, and we're in one now
						array_pop($stk);
						$c++;
						for ($i = $top['where']; $i <= $c; ++$i)
							$chrs = substr_replace($chrs, ' ', $i, 1);
					}
				}
				if (reset($stk) == JSON_IN_ARR)
					return ($arr);
				elseif (reset($stk) == JSON_IN_OBJ)
					return ($obj);
			}
	}
}

?>